package org.budo.maven.plugin.assembly.mojo;

import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.budo.compress.Compressor;
import org.budo.compress.CompressorFactory;
import org.budo.support.lang.util.FileUtil;
import org.budo.support.lang.util.IoUtil;
import org.budo.support.lang.util.ListUtil;
import org.budo.support.lang.util.ProcessUtil;
import org.budo.support.lang.util.StringUtil;
import org.budo.support.yaml.util.YamlUtil;
import org.budo.time.Time;
import org.codehaus.plexus.util.xml.Xpp3Dom;

import lombok.Getter;
import lombok.Setter;

/**
 * maven 打包插件
 * 
 * @author lmw
 * @see org.apache.maven.plugins.dependency.fromConfiguration.UnpackMojo
 * @see org.apache.maven.plugins.assembly.mojos.SingleAssemblyMojo
 */
@Mojo( //
        name = "budo-assembly", //
        defaultPhase = LifecyclePhase.PACKAGE, //
        threadSafe = true, //
        requiresDependencyResolution = ResolutionScope.COMPILE //
)
@Getter
@Setter
public class BudoMavenAssemblyMojo extends AbstractBudoMavenAssemblyMojo {
    private static final String[] BIN_FILES = new String[] { "README.txt", "dump.sh", "start.sh", "stop.sh", "start.bat", "stop.bat", "update.bat", "app.exe" }; // TODO 应该可以改成扫描

    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    private MavenProject project;

    @Parameter(defaultValue = "${session}", readonly = true, required = true)
    private MavenSession session;

    @Parameter
    private String format = "tar"; // tar.gz tar zip

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        try {
            this.getLog().info("#62 project.artifact=" + this.getProject().getArtifact() //
                    + ", project.base=" + this.getProject().getBasedir() + ", format=" + this.getFormat() + ", this=" + this);

            Properties activeProfileProperties = this.getActiveProfileProperties();

            // 清理目标文件夹
            String destPath = this.getProject().getBuild().getDirectory() + File.separator + this.getProject().getArtifactId() + "-assembly";
            FileUtil.deleteIfExists(destPath);

            // 复制依赖
            this.copyLib();

            // 复制conf
            this.copyConf(activeProfileProperties);

            // 将 SpringBoot 的 application.yml 转为 application.properties，在copyConf后执行
            // this.convertYamlToProperties();

            // 写入git版本，到conf
            this.writeGitLog();

            // 复制webapp
            this.copyWebapp();

            // 复制bin
            this.writeBin();

            // 模拟 maven-war-plugin 执行 资源占位符替换
            this.fakeMavenWarPluginWebResourceFiltering(activeProfileProperties);

            // 将自身Jar包 去掉resources后 放入lib
            this.copySelfJar();

            this.writeReadme();

            // 压缩
            this.compress();
        } catch (Throwable e) {
            this.getLog().error("#88 doExecute error, e=" + e, e);
            throw new MojoExecutionException("#89 BudoMavenAssemblyMojo Error, e=" + e, e);
        }
    }

    private void fakeMavenWarPluginWebResourceFiltering(Properties activeProfileProperties) {
        List<Plugin> plugins = this.getProject().getBuildPlugins();
        for (Plugin plugin : plugins) {
            String artifactId = plugin.getArtifactId();
            if (StringUtil.equalsIgnoreCase(artifactId, "maven-war-plugin")) {
                this._fakeMavenWarPluginWebResourceFiltering(plugin, activeProfileProperties);
            }
        }
    }

    private void _fakeMavenWarPluginWebResourceFiltering(Plugin plugin, Properties activeProfileProperties) {
        Object configuration = plugin.getConfiguration();
        if (null == configuration) {
            this.getLog().error("#122 configuration is null, plugin=" + plugin);
            return;
        }

        if (!(configuration instanceof Xpp3Dom)) {
            this.getLog().error("#128 configuration is not Xpp3Dom, type=" + configuration.getClass() + ", configuration=" + configuration + ", plugin=" + plugin);
            return;
        }

        Xpp3Dom conf = (Xpp3Dom) configuration;
        Xpp3Dom webResources = conf.getChild("webResources");
        if (null == webResources) {
            this.getLog().error("#147 configuration no webResources, configuration=" + configuration + ", plugin=" + plugin);
            return;
        }

        Xpp3Dom[] resources = webResources.getChildren("resource");
        if (null == resources || resources.length < 1) {
            this.getLog().error("#153 webResources no resource, configuration=" + configuration + ", plugin=" + plugin);
            return;
        }

        for (Xpp3Dom resource : resources) {
            String filtering = resource.getChild("filtering").getValue();
            if (!"true".equalsIgnoreCase(filtering)) {
                continue;
            }

            String directory = resource.getChild("directory").getValue();
            Xpp3Dom includes = resource.getChild("includes");
            Xpp3Dom[] includeArray = includes.getChildren();
            for (Xpp3Dom xpp3Dom : includeArray) {
                String include = xpp3Dom.getValue();
                this.getLog().error("#157 directory=" + directory //
                        + ", directory.canonicalPath=" + FileUtil.getCanonicalPath(new File(directory)) //
                        + ", include=" + include //
                        + ", activeProfileProperties=" + activeProfileProperties);

                this.filterDir(new File(directory), include, activeProfileProperties);
            }
        }
    }

    private void filterDir(File file, String include, Properties activeProfileProperties) {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            for (File each : files) {
                this.filterDir(each, include, activeProfileProperties);
            }
        } else {
            if (include.startsWith("**/")) { // 暂时只支持这种配置
                if (file.getName().endsWith(include.substring(3))) {
                    File webInfDir = new File(this.getProject().getBuild().getDirectory() + File.separator //
                            + this.getProject().getArtifactId() + "-assembly" + File.separator + "webapp" + File.separator + "WEB-INF"); // target 中的 webapp 目录
                    File to = new File(webInfDir, file.getName());
                    this.filterFile(file, to, activeProfileProperties);
                }
            } else {
                // 其他配置抛异常
                throw new RuntimeException("#178 include=" + include);
            }
        }
    }

    /**
     * 将所有Maven依赖移到lib目录
     */
    private void copyLib() throws Throwable {
        MavenProject project = this.getProject();

        String target = project.getBuild().getDirectory();
        File libDir = new File(target + File.separator + project.getArtifactId() + "-assembly" + File.separator + "lib");

        Set<Artifact> projectArtifacts = project.getArtifacts();
        if (this.getLog().isDebugEnabled()) {
            this.getLog().debug("#199 copyLib, project.artifacts=" + projectArtifacts);
        } else {
            this.getLog().info("#201 copyLib, project.artifacts.size=" + (null == projectArtifacts ? null : projectArtifacts.size()));
        }

        for (Artifact projectArtifact : projectArtifacts) {
            this.getLog().info("#210 projectArtifact=" + projectArtifact + ", scope=" + projectArtifact.getScope());

            File artifactFile = projectArtifact.getFile();
            if (artifactFile.isDirectory()) { // workspace dependency
                Artifact localArtifact = this.getSession().getLocalRepository().find(projectArtifact);
                this.getLog().warn("#107 workspace dependency, artifactFile=" + artifactFile + ", localArtifact.file=" + localArtifact.getFile());

                artifactFile = localArtifact.getFile();
            }

            File to = new File(libDir, artifactFile.getName());
            this.getLog().info("#113 copy jar, " + artifactFile + " > " + to);
            FileUtil.copyFile(artifactFile, to);
        }
    }

    /**
     * 写入一个说明文本
     */
    private void writeReadme() throws IOException {
        String target = this.getProject().getBuild().getDirectory();
        String artifactId = this.getProject().getArtifactId();
        File assemblyDir = new File(target + File.separator + artifactId + "-assembly");

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        this.getLog().info("#124 writeReadme, > " + assemblyDir.getCanonicalPath() + ", classLoader=" + classLoader);

        File to = new File(assemblyDir.getCanonicalPath() + File.separator + "README.txt");

        String mergedScript = this.mergeTemplate(README_TEMPLATE);
        IoUtil.write(mergedScript, new FileOutputStream(to));
    }

    /**
     * 写入脚本
     */
    private void writeBin() throws IOException, Throwable {
        String target = this.getProject().getBuild().getDirectory();
        File binDir = new File(target + File.separator + this.getProject().getArtifactId() + "-assembly" + File.separator + "bin");

        // mkdirs
        binDir.mkdirs();

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        this.getLog().info("#124 writeBinFiles, " + StringUtils.join(BIN_FILES, ", ") + " > " + binDir.getCanonicalPath() + ", classLoader=" + classLoader);

        URL assemblyBin = classLoader.getResource("budo-maven-assembly-bin");

        for (String binFile : BIN_FILES) {
            InputStream from = this.binFileInputStream(binFile);
            if (null == from) {
                this.getLog().error("#241 binFile " + binFile + " not found, assemblyBin=" + assemblyBin);
            }

            File to = new File(binDir.getCanonicalPath() + File.separator + binFile);

            // exe
            if (binFile.endsWith(".exe")) {
                IoUtil.write(from, to);
                continue;
            }

            // 脚本
            String scriptTemplate = IoUtil.toString(from);
            String mergedScript = this.mergeTemplate(scriptTemplate);
            IoUtil.write(mergedScript, new FileOutputStream(to));
        }
    }

    private InputStream binFileInputStream(String binFile) throws IOException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        URL assemblyBin = classLoader.getResource("budo-maven-assembly-bin");

        URL from = classLoader.getResource("budo-maven-assembly-bin" + File.separator + binFile);
        if (null != from) {
            return from.openStream();
        }

        JarURLConnection jarURLConnection = (JarURLConnection) assemblyBin.openConnection();
        if (null == jarURLConnection) {
            this.getLog().error("#249 jarURLConnection is null, assemblyBin=" + assemblyBin);
            return null;
        }

        JarFile jarFile = jarURLConnection.getJarFile();
        if (null == jarFile) {
            this.getLog().error("#249 jarFile is null, jarURLConnection=" + jarURLConnection);
            return null;
        }

        Enumeration<JarEntry> enumeration = jarFile.entries();
        while (enumeration.hasMoreElements()) {
            JarEntry jarEntry = (JarEntry) enumeration.nextElement();
            if (jarEntry.getName().endsWith(binFile)) {
                return jarFile.getInputStream(jarEntry);
            }
        }

        this.getLog().warn("#279 binFile " + binFile + " not found, assemblyBin=" + assemblyBin);
        return null;
    }

    /**
     * 写入Git版本信息
     */
    private void writeGitLog() {
        try {
            List<String> command = ListUtil.toStringList("git", "log", "--graph", "-9"); // git log --graph -9
            InputStream inputStream = ProcessUtil.runCommand(command);

            String target = this.getProject().getBuild().getDirectory();
            File confDir = new File(target + File.separator + this.getProject().getArtifactId() + "-assembly" + File.separator + "conf");
            File to = new File(confDir, "git-log.txt");

            IoUtil.write(inputStream, to);
            this.getLog().info("#163 writing git-log.txt to " + FileUtil.getCanonicalPath(to));
        } catch (Throwable e) {
            this.getLog().error("#165 writeGitLog error, e=" + e, e);
        }
    }

    private void copyWebapp() throws IOException, Throwable {
        String webappPath = "src" + File.separator + "main" + File.separator + "webapp";
        File srcWebapp = new File(this.getProject().getBasedir().getCanonicalPath() + File.separator + webappPath); // webapp 目录 绝对路径

        if (!srcWebapp.exists()) {
            this.getLog().warn("#174 folder " + webappPath + " not exists, path=" + srcWebapp.getCanonicalPath());
            return;
        }

        File webappDir = new File(this.getProject().getBuild().getDirectory() + File.separator //
                + this.getProject().getArtifactId() + "-assembly" + File.separator + "webapp"); // target 中的 webapp 目录

        this.getLog().info("#181 copy webapp, " + srcWebapp.getCanonicalPath() + " > " + webappDir.getCanonicalPath());
        FileUtil.copyDir(srcWebapp, webappDir);
    }

    /**
     * 将 SpringBoot 的 application.yml 转为 application.properties
     */
    @SuppressWarnings("unused")
    private void convertYamlToProperties() {
        try {
            this.doConvertYamlToProperties();
        } catch (Throwable e) {
            this.getLog().error("#285 convertYamlToProperties error", e);
        }
    }

    private void doConvertYamlToProperties() {
        File confDir = new File(this.getProject().getBuild().getDirectory() + File.separator //
                + this.getProject().getArtifactId() + "-assembly" + File.separator + "conf");

        File yml = new File(confDir, "application.yml");
        File props = new File(confDir, "application.properties");
        if (!yml.exists() || props.exists()) {
            this.getLog().info("#195 no application.yml or application.properties, confDir=" + FileUtil.getCanonicalPath(confDir));
            return;
        }

        InputStream ymlInputStream = IoUtil.fileInputStream(yml);
        Map<String, String> ymlMap = YamlUtil.ymlToMap(ymlInputStream);

        this.getLog().info("#195 application.yml found, " + FileUtil.getCanonicalPath(yml));

        String ymlProperties = "#\n";
        ymlProperties += "#Created By budo-maven-assembly-plugin from application.yml\n";
        ymlProperties += "#" + Time.now().toString("yyyy-MM-dd HH:mm:ss.SSS") + "\n";
        ymlProperties += "#\n\n";

        for (Entry<String, String> entry : ymlMap.entrySet()) {
            ymlProperties += entry.getKey() + "=" + entry.getValue() + "\n\n";
        }

        IoUtil.write(ymlProperties.getBytes(), props);
    }

    private void copyConf(Properties activeProfileProperties) throws IOException, Throwable {
        List<Resource> resources = this.getProject().getResources();
        this.getLog().info("#233 resources=" + resources);

        File confDir = new File(this.getProject().getBuild().getDirectory() //
                + File.separator + this.getProject().getArtifactId() + "-assembly"//
                + File.separator + "conf"); // resources 复制到 conf

        for (final Resource resource : resources) {
            FileFilter fileFilter = new FileFilter() {
                public boolean accept(File file) {
                    if (BudoMavenAssemblyMojo.this.matchAnyExcludes(file, resource)) {
                        return false; // 如果 match 就不复制
                    }

                    return true;
                }
            };

            File from = new File(resource.getDirectory());
            this.copyDirAndFiltering(from, confDir, fileFilter, resource.getFiltering(), activeProfileProperties);

            this.getLog().info("#224 copy conf, " + from.getCanonicalPath() + " > " + confDir.getCanonicalPath());
        }
    }

    private void copySelfJar() throws Throwable {
        if ("war".equalsIgnoreCase(this.getProject().getPackaging())) {
            this._copySelfWar();
        } else {
            this._copySelfJar();
        }
    }

    private void _copySelfWar() throws Exception {
        String target = this.getProject().getBuild().getDirectory();

        String jarName = this.getProject().getArtifactId() + "-" + this.getProject().getVersion() + ".jar";

        File jarTo = new File(target //
                + File.separator + this.getProject().getArtifactId() + "-assembly" //
                + File.separator + "lib" //
                + File.separator + jarName);

        File classesDir = new File(target, "classes");

        this.getLog().info("#251 _copySelfWar, from " + FileUtil.getCanonicalPath(classesDir) + ", to " + FileUtil.getCanonicalPath(jarTo));

        ZipOutputStream zipOut = new ZipOutputStream(IoUtil.fileOutputStream(jarTo));

        this._copySelfWar_2(classesDir, zipOut, null);

        zipOut.close();
    }

    private void _copySelfWar_2(File dir, ZipOutputStream zipOut, String path) throws IOException {
        File[] listFiles = dir.listFiles();
        for (File file : listFiles) {
            String entryPath = StringUtil.isEmpty(path) ? file.getName() : path + File.separator + file.getName();
            if (file.isDirectory()) {
                this._copySelfWar_2(file, zipOut, entryPath); // 递归
            } else {
                String resourcePath = this.findInResource(entryPath);
                if (null != resourcePath) { // 新的 jar 中跳过 resources 里面的文件
                    this.getLog().debug("#279 skip resource file " + file + ", " + resourcePath);
                    continue;
                }

                this.getLog().debug("#283 add selfJar entry " + file);

                zipOut.putNextEntry(new ZipEntry(entryPath));
                InputStream inputStream = IoUtil.fileInputStream(file);
                IoUtil.write(inputStream, zipOut, false, true);
                zipOut.closeEntry();
            }
        }
    }

    private void _copySelfJar() throws Throwable {
        String target = this.getProject().getBuild().getDirectory();
        String jarName = this.getProject().getArtifactId() + "-" + this.getProject().getVersion() + ".jar";

        File jarTo = new File(target //
                + File.separator + this.getProject().getArtifactId() + "-assembly" //
                + File.separator + "lib" //
                + File.separator + jarName);

        File jarFrom;

        String jarOriginal = this.getProject().getArtifactId() + "-" + this.getProject().getVersion() + ".jar.original";
        File jarOriginalFile = new File(target + File.separator + jarOriginal);

        if (jarOriginalFile.exists()) {
            jarFrom = jarOriginalFile; // spring-boot 项目
        } else {
            jarFrom = new File(target + File.separator + jarName); // 非 spring-boot 非 war 项目
        }

        this.getLog().info("#247 copySelfJar, jarFrom=" + jarFrom + ", jarTo=" + jarTo);
        this.copyJarSkipResources(jarFrom, jarTo);
    }

    /**
     * 复制 jar 排除 resource
     * 
     * @param from jar文件原位置
     * @param to   lib目录中的这个jar的目标位置
     */
    private void copyJarSkipResources(File from, File to) throws Throwable {
        if (!from.exists()) {
            this.getLog().warn("#257 selfJar not exists, " + from);
            return;
        }

        ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(to));

        // 遍历jar
        ZipFile zipFile = new ZipFile(from);
        Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();

        while (zipEntries.hasMoreElements()) {
            ZipEntry zipEntry = zipEntries.nextElement();

            String resourcePath = this.findInResource(zipEntry.getName());
            if (null != resourcePath) { // 新的 jar 中跳过 resources 里面的文件
                this.getLog().debug("#276 skip resource file " + zipEntry + ", " + resourcePath);
                continue;
            }

            this.getLog().debug("#280 add selfJar entry " + zipEntry);

            zipOut.putNextEntry(zipEntry);
            InputStream inputStream = zipFile.getInputStream(zipEntry);
            IoUtil.write(inputStream, zipOut, false, true);
            zipOut.closeEntry();
        }

        zipOut.close();
        zipFile.close();
    }

    /**
     * 压缩
     */
    private void compress() throws IOException {
        String target = this.getProject().getBuild().getDirectory();

        File dir = new File(target + File.separator + this.getProject().getArtifactId() + "-assembly");

        String[] fmts = this.getFormat().split(",");
        for (String fmt : fmts) {
            File zipFile = new File(target + File.separator + this.getProject().getArtifactId() + "-assembly" + "." + fmt.trim());

            Compressor compressor = CompressorFactory.getCompressor(zipFile.getName());
            compressor.compress(dir, zipFile);

            this.getLog().info("#304 done, compressor=" + compressor + ", canonicalPath=" + zipFile.getCanonicalPath() + ", this=" + this);
        }
    }
}
